Symmetry reduction

Adapted from: https://github.com/kalmarek/SymbolicWedderburn.jl/blob/tw/exsos/examples/exC4.jl

using Pkg
pkg"add https://github.com/kalmarek/SymbolicWedderburn.jl#tw/ex_sos"

import MutableArithmetics
const MA = MutableArithmetics
using MultivariatePolynomials
const MP = MultivariatePolynomials
using MultivariateBases
const MB = MultivariateBases

using DynamicPolynomials
@polyvar x[1:4]
(DynamicPolynomials.PolyVar{true}[x₁, x₂, x₃, x₄],)

We would like to find the minimum value of the polynomial

poly = sum(x) + sum(x.^2)
\[ x_{1}^{2} + x_{2}^{2} + x_{3}^{2} + x_{4}^{2} + x_{1} + x_{2} + x_{3} + x_{4} \]

As we can decouple the problem for each x[i] for which x[i] + x[i]^2 has minimum value 0.25, we would expect to get -1 as answer. Can this decoupling be exploited by SumOfSquares as well ? For this, we need to use a certificate that can exploit the permutation symmetry of the polynomial. This is still a work in progress in SumOfSquares, so we need to define things here:

using SymbolicWedderburn
using PermutationGroups
using Cyclotomics
using SumOfSquares

function SymbolicWedderburn.OnPoints(basis::Union{MonomialVector, AbstractVector{<:Monomial}})
    basis_exps = Vector{Vector{Int}}(undef, length(basis))
    basis_dict = Dict{Vector{Int}, Int}()
    sizehint!(basis_dict, length(basis))

    for (i, b) in enumerate(basis)
        e = MP.exponents(b) # so that we allocate exponents only once
        basis_exps[i] = e
        basis_dict[e] = i
    end

    return SymbolicWedderburn.OnPoints(basis_exps, basis_dict)
end
function symmetry_adapted_basis(G, mvec)
    chars_vars = SymbolicWedderburn.characters_dixon(G)

    chars = chars_vars
    basis = mvec

    @assert all(χ.inv_of == first(chars).inv_of for χ in chars)

    induced_action = SymbolicWedderburn.OnPoints(basis)
    ccG_large = induced_action.(conjugacy_classes(first(chars)))

    let ccls = ccG_large, large_gens = induced_action.(gens(G)) # double check:
        G_large = PermGroup(large_gens)
        ccG_large = conjugacy_classes(G_large)
        @assert all(Set.(collect.(ccG_large)) .== Set.(collect.(ccls)))
    end

    chars_mvec = [SymbolicWedderburn.Character(values(χ), χ.inv_of, ccG_large) for χ in chars]

    vr_chars = SymbolicWedderburn.real_vchars(chars_mvec)
    U = filter!(x->all(!iszero, x), [SymbolicWedderburn.matrix_projection(χ) for χ in vr_chars])
    R = map(U) do c_u
        u = last(c_u)
        if all(isreal, u)
            image_coeffs, pivots = SymbolicWedderburn.row_echelon_form(float.(u))
            dim = length(pivots)
            image_coeffs[1:dim, :]
        else
            throw("Not Implemented")
        end
    end

    return map(R) do Ri
        FixedPolynomialBasis(Ri * mvec)
    end
end
function MP.polynomialtype(::Type{<:MB.AbstractPolynomialVectorBasis{PT}}, T::Type) where PT
    C = MP.coefficienttype(PT)
    U = MA.promote_operation(*, C, T)
    V = MA.promote_operation(+, U, U)
    return MP.polynomialtype(PT, V)
end
struct SymmetricIdeal{CT, GT} <: Certificate.AbstractIdealCertificate
    cone::CT
    group::GT
end
SumOfSquares.matrix_cone_type(::Type{<:SymmetricIdeal{CT}}) where {CT} = SumOfSquares.matrix_cone_type(CT)
Certificate.get(::Type{<:SymmetricIdeal}, ::SumOfSquares.Certificate.GramBasisType) = Vector{MB.FixedPolynomialBasis}
Certificate.zero_basis_type(::Type{<:SymmetricIdeal}) = MB.MonomialBasis
Certificate.zero_basis(::SymmetricIdeal) = MB.MonomialBasis
Certificate.get(::SymmetricIdeal, ::Certificate.ReducedPolynomial, poly, domain) = poly
function Certificate.get(cert::SymmetricIdeal, ::Certificate.GramBasis, poly)
    basis = Certificate.maxdegree_gram_basis(MB.MonomialBasis, MP.variables(poly), MP.maxdegree(poly))
    return symmetry_adapted_basis(cert.group, basis.monomials)
end
G = PermGroup([perm"(1,2,3,4)"])
Permutation group on 1 generators
⟨(1,2,3,4)⟩

We can use this certificate as follows:

import CSDP
solver = CSDP.Optimizer
model = Model(solver)
@variable(model, t)
@objective(model, Max, t)
con_ref = @constraint(model, sum(x) + sum(x.^2) - t in SOSCone(), ideal_certificate = SymmetricIdeal(SOSCone(), G))
optimize!(model)
value(t)
  • 1 . 0 0 0 0 0 0 0 0 0 4 5 0 4 8 4 8

We indeed find -1, let's verify that symmetry was exploited:

gram_matrix(con_ref)
SparseGramMatrix{Float64,FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}},Float64,SymMatrix{Float64}}(GramMatrix{Float64,FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}},Float64,SymMatrix{Float64}}[GramMatrix{Float64,FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}},Float64,SymMatrix{Float64}}([0.25000000000000044], FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}}(DynamicPolynomials.Polynomial{true,Float64}[x₁ - x₂ + x₃ - x₄])), GramMatrix{Float64,FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}},Float64,SymMatrix{Float64}}([0.2500000000000032 0.4999999999999974; 0.4999999999999974 1.0000000004504837], FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}}(DynamicPolynomials.Polynomial{true,Float64}[x₁ + x₂ + x₃ + x₄, 1.0])), GramMatrix{Float64,FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}},Float64,SymMatrix{Float64}}([0.5000000000000009 -4.465962610364332e-16; -4.465962610364332e-16 0.5000000000000012], FixedPolynomialBasis{DynamicPolynomials.Polynomial{true,Float64},Array{DynamicPolynomials.Polynomial{true,Float64},1}}(DynamicPolynomials.Polynomial{true,Float64}[x₁ - x₃, x₂ - x₄]))])

This page was generated using Literate.jl.